package com.github.cyf.interceptor.page;

import com.github.cyf.interceptor.page.dialect.Dialect;
import com.github.cyf.util.MSUtil;
import com.github.cyf.util.PrintUtil;
import lombok.Data;
import lombok.Getter;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Mybatis 分页拦截器
 * @author chenyifan
 * @create 2024-04-13 18:52
 */
@Intercepts({
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
        ),
        @Signature(
                type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
        )
})
@Data
public class PageInterceptor implements Interceptor {

    //第一个分页参数
    private static final String PAGE_PARAMETER_FIRST = "First_PageHelper";
    //第二个分页参数
    private static final String PAGE_PARAMETER_SECOND = "Second_PageHelper";

    private static PrintUtil printUtil = new PrintUtil(PageInterceptor.class);

    private Dialect dialect;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        printUtil.printInterceptorBegin();
        try {
            // page 为 null 说明不分页，直接执行被代理方法
            Page page = PageHelper.getLocalPage();
            if (page == null) {
                return invocation.proceed();
            }
            Executor executor = (Executor) invocation.getTarget();
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            BoundSql boundSql;
            CacheKey cacheKey;
            if (args.length == 4) {
                // 4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                // 6 个参数时
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }

            // 获取总记录数
            Integer totalCount = countWay(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            page.setTotal(totalCount);

            // 获取分页结果
            // 不能选择 Executor 的 query 方法（接受四个参数版本）执行 SQL，
            // 比如 getAllUser 方法的 sql 为 SELECT * FROM t_user
            // 拦截器需要改变 MappedStatement 的 sqlSource 属性才能保证分页查询，
            // 而拦截器将 getAllUser 方法的 sql 改为 SELECT * FROM t_user LIMIT ? , ?，会发现违背了 getAllUser 方法的意图
            BoundSql pageBoundSql = getPageBoundSql(ms, parameter, page, boundSql);
            List list = executor.query(ms, pageBoundSql.getParameterObject(), rowBounds, resultHandler, cacheKey, pageBoundSql);
            page.addAll(list);
            return page;
        } finally {
            PageHelper.clearLocalPage();
            printUtil.printInterceptorEnd();
        }
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * count 查询方式
     *
     * 相比之下，四个参数的 query 方法并不直接包含 BoundSql，意味着日志拦截器如果需要打印 SQL 语句，
     * 需要额外步骤去生成 BoundSql 对象，然而，通过 BoundSql 对象获取的 SQL 是原始 SQL，它可能尚未经过拦截器（如租户拦截器）的修改。
     * 如果你的应用使用了租户拦截器在 SQL 预编译阶段对 SQL 进行了修改（例如，添加了租户ID等），
     * 那么仅通过 BoundSql 对象打印的 SQL 将是原始 SQL，而不是实际执行时的 SQL。
     */
    private Integer countWay(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                              ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        String countSql = dialect.getCountSql(boundSql.getSql());
        // 新建 count 查询的 MappedStatement
        MappedStatement countMs = MSUtil.newCountMappedStatement(ms, countSql, boundSql.getParameterMappings());

        // 获取  countBoundSql
        BoundSql countBoundSql = new BoundSql(countMs.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);

        // 传入的 cacheKey 不能为空
        // 如果对同一个 Executor 进行多次查询（sql不一样）都传入 null 作为 CacheKey
        // MyBatis 将误认为这是对相同查询的缓存请求，会导致后续查询从缓存中获取结果，拿到别的 sql 的执行结果
        CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql);
        // 执行 count 查询
        List countResultList = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countBoundSql);
        Integer count = (Integer) countResultList.get(0);
        return count;
    }

    /**
     * 根据原始 sql 信息，获取分页 sql 信息
     * @param ms
     * @param parameter 参数值
     * @param page 分页参数
     * @param boundSql 原始 sql 信息
     * @return
     */
    private BoundSql getPageBoundSql(MappedStatement ms, Object parameter, Page page, BoundSql boundSql) {
        Map<String, Object> paramMap = parameter == null ? new HashMap() : (Map<String, Object>) parameter;
        // 添加两个分页参数值
        paramMap.put(PAGE_PARAMETER_FIRST, (page.getPageNo() - 1) * page.getPageSize());
        paramMap.put(PAGE_PARAMETER_SECOND, page.getPageSize());
        // 添加 sql 参数映射
        List<ParameterMapping> newParameterMappings = new ArrayList<>(boundSql.getParameterMappings());
        newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGE_PARAMETER_FIRST, int.class).build());
        newParameterMappings.add(new ParameterMapping.Builder(ms.getConfiguration(), PAGE_PARAMETER_SECOND, int.class).build());
        return new BoundSql(ms.getConfiguration(), dialect.getPageSql(boundSql.getSql()), newParameterMappings, paramMap);
    }

}